import aphw._aphw as impl

import os, weakref

import aphw
from aphw.record import GetRecord
from aphw.util import check_writeable, return_status

@return_status
def _open(db, filename, flags, params):
    return impl.open(db, filename, flags, params)

@return_status
def _create(db, filename, flags, params):
    if filename is None:
        return impl.create_memory(db, flags, params)
    else:
        return impl.create(db, filename, flags, params)

@return_status
def _close(db, flags):
    return impl.close(db, flags)

@return_status
def _insert(db, key, record, flags,
            partial_offset,
            partial_size):
    if (partial_offset is not None) and (partial_size is not None):
        flags |= (aphw.Flags.PARTIAL | aphw.Flags.OVERWRITE);
    else:
        partial_offset = 0
        partial_size = 0

    return impl.insert(db, 
                       key._key, 
                       record, 
                       flags,
                       partial_offset,
                       partial_size)

@return_status
def _find(db, key, record, flags, 
          partial_offset,
          partial_size):
    if (partial_offset is not None) and (partial_size is not None):
        flags |= aphw.Flags.PARTIAL;
    else:
        partial_offset = 0
        partial_size = 0

    return impl.find(db, 
                     key._key, 
                     record, 
                     flags,
                     partial_offset,
                     partial_size)

@return_status
def _erase(db, key, flags):
    return impl.erase(db, key._key, flags)

@return_status
def _flush(db, flags):
    return impl.flush(db, flags)

def _key_count(db, flags):
    err, count = impl.key_count(db, flags)
    return (aphw.StatusCodes.values[err], count)

def _get_params(db, param_ids):
    err, params = impl.get_params(db, param_ids)
    return (aphw.StatusCodes.values[err], params)

def _get_flags(db):
    return impl.get_flags(db)

def _get_error(db):
    return impl.get_error(db)

class Database(object):

    def __init__(self, 
                 filename=None,
                 read_only=False,
                 write_through=False,
                 use_btree=True,
                 disable_var_keylen=False,
                 record_number=False,
                 enable_duplicates=False,
                 disable_mmap=False,
                 cache_size_policy = aphw.CacheSizePolicy.NORMAL,
                 enable_recovery=False,
                 enable_auto_recovery=False,
                 enable_transactions=True,
                 params = None,
                 db = None):
        '''Construct a new Database

        If `filename` exists, then it will be opened. Otherwise, a new
        database will be created.

        TODO: Redo this docstring
        '''

        self._cursors = weakref.WeakValueDictionary()

        if db is not None:
            self.__read_only = read_only
            self._db = db
            return

        self._db = impl.Database()

        if params is None:
            params = {}

        if filename is None or not os.path.exists(filename):
            self.__read_only = False
            err = self._create(filename=filename,
                               write_through=write_through,
                               use_btree=use_btree,
                               disable_var_keylen=disable_var_keylen,
                               record_number=record_number,
                               enable_duplicates=enable_duplicates,
                               disable_mmap=disable_mmap,
                               cache_size_policy=cache_size_policy,
                               enable_recovery=enable_recovery,
                               enable_transactions=enable_transactions,
                               params=params)
        else:
            self.__read_only = read_only
            err = self._open(filename=filename,
                             read_only=read_only,
                             write_through=write_through,
                             disable_var_keylen=disable_var_keylen,
                             disable_mmap=disable_mmap,
                             cache_size_policy=cache_size_policy,
                             enable_recovery=enable_recovery,
                             enable_auto_recovery=enable_auto_recovery,
                             enable_transactions=enable_transactions,
                             params=params)

        # If there was any error other than not-found, 
        # abort the construction
        if err != aphw.StatusCodes.SUCCESS:
            raise aphw.StatusError(err, 'Error opening database file')
        
    def _open(self,
              filename,
              read_only,
              write_through,
              disable_var_keylen,
              disable_mmap,
              cache_size_policy,
              enable_recovery,
              enable_auto_recovery,
              enable_transactions,
              params):

        flags = 0

        if read_only:
            flags |= aphw.Flags.READ_ONLY
        if write_through:
            flags |= aphw.Flags.WRITE_THROUGH
        if disable_var_keylen:
            flags |= aphw.Flags.DISABLE_VAR_KEYLEN
        if disable_mmap:
            flags |= aphw.Flags.DISABLE_MMAP
        if enable_auto_recovery:
            flags |= aphw.Flags.ENABLE_AUTO_RECOVERY
            enable_recovery = True
        if enable_recovery:
            if write_through:
                raise ValueError('write-through not allowed with enable-recovery')                
            flags |= aphw.Flags.ENABLE_RECOVERY
        if enable_transactions:
            flags |= aphw.Flags.ENABLE_TRANSACTIONS

        if cache_size_policy == aphw.CacheSizePolicy.STRICT:
            flags |= aphw.Flags.CACHE_STRICT
        elif cache_size_policy == aphw.CacheSizePolicy.UNLIMITED:
            flags |= aphw.Flags.CACHE_UNLIMITED
        
        return _open(self._db,
                     filename,
                     flags,
                     params)

    def _create(self,
                filename,
                write_through,
                use_btree,
                disable_var_keylen,
                record_number,
                enable_duplicates,
                disable_mmap,
                cache_size_policy,
                enable_recovery,
                enable_transactions,
                params):
        flags = 0

        if write_through:
            flags |= aphw.Flags.WRITE_THROUGH
        if use_btree:
            flags |= aphw.Flags.USE_BTREE
        if disable_var_keylen:
            flags |= aphw.Flags.DISABLE_VAR_KEYLEN
        if record_number:
            flags |= aphw.Flags.RECORD_NUMBER
        if enable_duplicates:
            flags |= aphw.Flags.ENABLE_DUPLICATES
        if disable_mmap:
            flags |= aphw.Flags.DISABLE_MMAP
        if enable_transactions:
            enable_recovery = True
            flags |= aphw.Flags.ENABLE_TRANSACTIONS
        if enable_recovery:
            if filename is None:
                raise ValueError('Memory databases not allowed with enable-recovery')
            if write_through:
                raise ValueError('write-through not allowed with enable-recovery')
            flags |= aphw.Flags.ENABLE_RECOVERY

        if cache_size_policy == aphw.CacheSizePolicy.STRICT:
            flags |= aphw.Flags.CACHE_STRICT
        elif cache_size_policy == aphw.CacheSizePolicy.UNLIMITED:
            flags |= aphw.Flags.CACHE_UNLIMITED
            
        if aphw.Params.CACHESIZE in params:
            if cache_size_policy == aphw.CacheSizePolicy.UNLIMITED:
                raise ValueError('unlimited-cache not allowed with cachesize param')

        if filename is None:
            flags |= aphw.Flags.IN_MEMORY_DB
            
            if cache_size_policy == aphw.CacheSizePolicy.STRICT:
                raise ValueError('Memory databases not allowed with cache-size-strict')

            try:
                if params[aphw.Params.CACHE_SIZE] != 0:
                    raise ValueError('Memory database can not have a cachesize param other than 0')
            except KeyError:
                pass

        return _create(self._db, 
                       filename, 
                       flags, 
                       params)
        
    read_only = property(lambda self: self.__read_only)

    def close(self):
        '''Close an open database.
        
        This will abort any existing transactions and close any
        open cursors.
        '''

        # Close all cursors
        for c in self._cursors.itervalues():
            c.close()

        err = _close(self._db,
                     aphw.Flags.TXN_AUTO_ABORT)

        assert err == aphw.StatusCodes.SUCCESS

    def get_parameters(self, ids):
        '''Get a mapping of parameters and values from the database.

        :param ids: The IDs of the parameters to retrieve
        
        :return: A mapping of parameters IDs to parameter values
        
        :except StatusError: If there is an error retrieving
          the parameters
        '''

        err, params = _get_params(self._db, ids)

        if err != aphw.StatusCodes.SUCCESS:
            raise aphw.StatusError(
                err, 
                'Error getting parameters: %s' % err)

        return params

    def get_flags(self):
        '''Get the flags for the database

        :return: The OR'd flags
        '''
        return _get_flags(self._db)

    def get_error(self):
        '''Get the last error code

        :return: The last error code returned by a method
        '''
        return _get_error(self._d)

    # TODO: ham_set_prefix_compare_func (ham_db_t *db, ham_prefix_compare_func_t foo)
    # TODO: ham_set_compare_func (ham_db_t *db, ham_compare_func_t foo)
    # TODO: ham_enable_compression (ham_db_t *db, ham_u32_t level, ham_u32_t flags)

    @check_writeable
    def insert(self,
               key, 
               record, 
               overwrite=False,
               flags=0,
               partial_offset=None,
               partial_size=None):
        '''Insert a key-value pair into the database

        :param key: The integer key for the row, or an aphw.Key
                    instance. If `key` is a Key instance, and if
                    `record_number` was used to create this database,
                    then `key` will be populated with the new row
                    value as appropriate.
        
        :param record: The record for the row. This can be any object
                       that provides a buffer interface.
        
        :param overwrite: Allow new row to overwrite an existing row
                          with the same key

        :param flags: Other flags to pass to the insert call (see:
                      aphw.Flags)
        
        :except ReadOnlyException: If the database is read-only

        :except KeyError: If you attempt to overwrite an existing key
                          without specifying `overwrite`

        :except StatusError: If there is any other errors inserting the
                             row
        '''

        if not isinstance(key, aphw.Key):
            key = aphw.Key(key)
        record = aphw.Record(record)
        
        if overwrite:
            flags |= aphw.Flags.OVERWRITE

        err = _insert(self._db, 
                      key, 
                      record._rec, 
                      flags,
                      partial_offset,
                      partial_size)

        if err == aphw.StatusCodes.DB_READ_ONLY:
            raise aphw.ReadOnlyException(
                'Can not into read-only database')

        elif err == aphw.StatusCodes.DUPLICATE_KEY:
            raise KeyError('Inserting duplicate key: %s' % key)

        elif err != aphw.StatusCodes.SUCCESS:
            raise aphw.StatusError(
                err, 
                'Error inserting record: %s -> %s' % (key, record))

    def find(self, key, flags=0, 
             partial_offset=None, 
             partial_size=None):
        '''Find a record with a given key
        
        If both `partial_offset` and `partial_size` are not None, then
        they are used as the bounds of a partial read. Otherwise, a
        full read is performed.

        :param key: The key of the record to find

        :param flags: Other flags to pass to the insert call (see:
          aphw.Flags)

        :param partial_offset: The starting position for a partial
          read.

        :param partial_size: The size of a partial read.

        :except IndexError: If a partial read would be outside the
          bounds of the record.

        :except StatusError: If there are any other errors finding the
          row
        '''
        if not isinstance(key, aphw.Key):
            key = aphw.Key(key)

        rec = impl.GetRecord()
        err = _find(self._db, key, rec, flags,
                    partial_offset,
                    partial_size)

        if err == aphw.StatusCodes.KEY_NOT_FOUND:
            raise KeyError('Key not found in database: %s' % key)

        elif err != aphw.StatusCodes.SUCCESS:
            raise aphw.StatusError(
                err,
                'Error finding record: %s' % key)

        return aphw.record.GetRecord(rec)

    @check_writeable
    def erase(self, key, flags=0):
        if not isinstance(key, aphw.Key):
            key = aphw.Key(key)
        
        err = _erase(self._db, key, flags)
        
        if err == aphw.StatusCodes.DB_READ_ONLY:
            raise aphw.ReadOnlyException(
                'Can not erase from read-only database')
        elif err == aphw.StatusCodes.KEY_NOT_FOUND:
            raise KeyError('Key not found in database: %s' % key)
        

        elif err != aphw.StatusCodes.SUCCESS:
            raise aphw.StatusError(
                err,
                'Error erasing record: %s' % key)

    def flush(self, flags=0):
        err = _flush(self._db, flags)
        if err != aphw.StatusCodes.SUCCESS:
            raise aphw.StatusError(
                err,
                'Error flushing database')

    def key_count(self, flags=0):
        (err, count) = _key_count(self._db, flags)
        if err != aphw.StatusCodes.SUCCESS:
            raise aphw.StatusError(
                err,
                'Error getting key count')
        return count

    def __iter__(self):
        return aphw.Cursor(self)

    def __enter__(self):
        return self

    def __exit__(self, type, instance, traceback):
        self.close()
        return False

    # dict interface
#     def __setitem__(self,
#                     key,
#                     record):
#         self.insert(key, record, overwrite=True)

#     def __getitem__(self, key):
#         return self.find(key)

#     def __delitem__(self, key):
#         return self.erase(key)

#     def __len__(self):
#         return self.key_count()

#     def items(self):
#         pass

#     def keys(self):
#         pass

#     def values(self):
#         pass 
